/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.solr.servlet;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import jakarta.servlet.ReadListener;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.invoke.MethodHandles;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.params.CommonParams;
import org.apache.solr.common.params.MultiMapSolrParams;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.common.util.ContentStream;
import org.apache.solr.common.util.StrUtils;
import org.apache.solr.core.SolrCore;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.servlet.SolrRequestParsers.FormDataRequestParser;
import org.apache.solr.servlet.SolrRequestParsers.MultipartRequestParser;
import org.apache.solr.servlet.SolrRequestParsers.RawRequestParser;
import org.apache.solr.servlet.SolrRequestParsers.StandardRequestParser;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class SolrRequestParserTest extends SolrTestCaseJ4 {

  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

  @BeforeClass
  public static void beforeClass() throws Exception {
    assumeWorkingMockito();
    System.setProperty("solr.requests.streaming.remote.enabled", "true");
    System.setProperty("solr.requests.streaming.body.enabled", "true");
    initCore("solrconfig.xml", "schema.xml");
    parser = new SolrRequestParsers(h.getCore().getSolrConfig());
  }

  static SolrRequestParsers parser;

  @AfterClass
  public static void afterClass() {
    parser = null;
  }

  @Test
  public void testStreamBody() throws Exception {
    String body1 = "AMANAPLANPANAMA";
    String body2 = "qwertasdfgzxcvb";
    String body3 = "1234567890";

    SolrCore core = h.getCore();

    Map<String, String[]> args = new HashMap<>();
    args.put(CommonParams.STREAM_BODY, new String[] {body1});

    // Make sure it got a single stream in and out ok
    List<ContentStream> streams = new ArrayList<>();
    try (SolrQueryRequest req =
        parser.buildRequestFrom(core, new MultiMapSolrParams(args), streams)) {
      assertNotNull(req);
      assertEquals(1, streams.size());
      assertEquals(body1, StrUtils.stringFromReader(streams.get(0).getReader()));
    }

    // Now add three and make sure they come out ok
    streams = new ArrayList<>();
    args.put(CommonParams.STREAM_BODY, new String[] {body1, body2, body3});
    try (SolrQueryRequest req =
        parser.buildRequestFrom(core, new MultiMapSolrParams(args), streams)) {
      assertNotNull(req);
      assertEquals(3, streams.size());
      ArrayList<String> input = new ArrayList<>();
      ArrayList<String> output = new ArrayList<>();
      input.add(body1);
      input.add(body2);
      input.add(body3);
      for (ContentStream cs : streams) {
        output.add(StrUtils.stringFromReader(cs.getReader()));
      }
      // sort them so the output is consistent
      Collections.sort(input);
      Collections.sort(output);
      assertEquals(input.toString(), output.toString());
    }

    // set the contentType and make sure that it gets set
    String ctype = "text/xxx";
    streams = new ArrayList<>();
    args.put(CommonParams.STREAM_CONTENTTYPE, new String[] {ctype});
    try (SolrQueryRequest req =
        parser.buildRequestFrom(core, new MultiMapSolrParams(args), streams)) {
      assertNotNull(req);
      for (ContentStream s : streams) {
        assertEquals(ctype, s.getContentType());
      }
    }
  }

  @Test
  @SuppressWarnings({"try"})
  public void testStreamURL() throws Exception {
    URL url = getClass().getResource("/README");
    assertNotNull("Missing file 'README' in test-resources root folder.", url);

    final byte[] bytes;
    try (InputStream inputStream = url.openStream()) {
      bytes = inputStream.readAllBytes();
    }

    SolrCore core = h.getCore();

    Map<String, String[]> args = new HashMap<>();
    args.put(CommonParams.STREAM_URL, new String[] {url.toExternalForm()});

    // Make sure it got a single stream in and out ok
    List<ContentStream> streams = new ArrayList<>();
    try (SolrQueryRequest req =
        parser.buildRequestFrom(core, new MultiMapSolrParams(args), streams)) {
      assertNotNull(req);
      assertEquals(1, streams.size());
      try (InputStream in = streams.get(0).getStream()) {
        assertArrayEquals(bytes, in.readAllBytes());
      }
    }
  }

  @Test
  @SuppressWarnings({"try"})
  public void testStreamFile() throws Exception {
    Path file = getFile("README");

    byte[] bytes = Files.readAllBytes(file);

    SolrCore core = h.getCore();

    Map<String, String[]> args = new HashMap<>();
    args.put(CommonParams.STREAM_FILE, new String[] {file.toAbsolutePath().toString()});

    // Make sure it got a single stream in and out ok
    List<ContentStream> streams = new ArrayList<>();
    try (SolrQueryRequest req =
        parser.buildRequestFrom(core, new MultiMapSolrParams(args), streams)) {
      assertNotNull(req);
      assertEquals(1, streams.size());
      try (InputStream in = streams.get(0).getStream()) {
        assertArrayEquals(bytes, in.readAllBytes());
      }
    }
  }

  @Test
  public void testUrlParamParsing() {
    final String[][] teststr =
        new String[][] {
          {"this is simple", "this%20is%20simple"},
          {"this is simple", "this+is+simple"},
          {"\u00FC", "%C3%BC"}, // lower-case "u" with diaeresis/umlaut
          {"\u0026", "%26"}, // &
          {"", ""}, // empty
          {"\u20AC", "%E2%82%ac"} // euro, also with lowercase escapes
        };

    for (String[] tst : teststr) {
      SolrParams params = SolrRequestParsers.parseQueryString("val=" + tst[1]);
      assertEquals(tst[0], params.get("val"));
      params = SolrRequestParsers.parseQueryString("val=" + tst[1] + "&");
      assertEquals(tst[0], params.get("val"));
      params = SolrRequestParsers.parseQueryString("&&val=" + tst[1] + "&");
      assertEquals(tst[0], params.get("val"));
      params = SolrRequestParsers.parseQueryString("&&val=" + tst[1] + "&&&val=" + tst[1] + "&");
      assertArrayEquals(new String[] {tst[0], tst[0]}, params.getParams("val"));
    }

    SolrParams params = SolrRequestParsers.parseQueryString("val");
    assertEquals("", params.get("val"));

    params = SolrRequestParsers.parseQueryString("val&foo=bar=bar&muh&");
    assertEquals("", params.get("val"));
    assertEquals("bar=bar", params.get("foo"));
    assertEquals("", params.get("muh"));

    final String[] invalid = {
      "q=h%FCllo", // non-UTF-8
      "q=h\u00FCllo", // encoded string is not pure US-ASCII
      "q=hallo%", // incomplete escape
      "q=hallo%1", // incomplete escape
      "q=hallo%XX123", // invalid digit 'X' in escape
      "=hallo" // missing key
    };
    for (String s : invalid) {
      expectThrows(SolrException.class, () -> SolrRequestParsers.parseQueryString(s));
    }
  }

  @Test
  public void testStandardParseParamsAndFillStreams() throws Exception {
    final String getParams = "qt=%C3%BC&dup=foo", postParams = "q=hello&d%75p=bar";
    final byte[] postBytes = postParams.getBytes(StandardCharsets.US_ASCII);

    // Set up the expected behavior
    final String[] ct =
        new String[] {
          "application/x-www-form-urlencoded",
          "Application/x-www-form-urlencoded",
          "application/x-www-form-urlencoded; charset=utf-8",
          "application/x-www-form-urlencoded;"
        };

    for (String contentType : ct) {
      HttpServletRequest request = getMock("/solr/select", contentType, postBytes.length);
      when(request.getMethod()).thenReturn("POST");
      when(request.getQueryString()).thenReturn(getParams);
      when(request.getInputStream()).thenReturn(new ByteServletInputStream(postBytes));

      MultipartRequestParser multipart = new MultipartRequestParser(2048);
      RawRequestParser raw = new RawRequestParser();
      FormDataRequestParser formdata = new FormDataRequestParser(2048);
      StandardRequestParser standard = new StandardRequestParser(multipart, raw, formdata);

      SolrParams p = standard.parseParamsAndFillStreams(request, new ArrayList<ContentStream>());

      assertEquals("contentType: " + contentType, "hello", p.get("q"));
      assertEquals("contentType: " + contentType, "\u00FC", p.get("qt"));
      assertArrayEquals(
          "contentType: " + contentType, new String[] {"foo", "bar"}, p.getParams("dup"));

      verify(request).getInputStream();
    }
  }

  static class ByteServletInputStream extends ServletInputStream {
    final BufferedInputStream in;
    final int len;
    int readCount = 0;

    public ByteServletInputStream(byte[] data) {
      this.len = data.length;
      this.in = new BufferedInputStream(new ByteArrayInputStream(data));
    }

    @Override
    public boolean isFinished() {
      return readCount == len;
    }

    @Override
    public boolean isReady() {
      return true;
    }

    @Override
    public void setReadListener(ReadListener readListener) {
      throw new IllegalStateException("Not supported");
    }

    @Override
    public int read() throws IOException {
      int read = in.read();
      readCount += read;
      return read;
    }
  }

  @Test
  public void testStandardParseParamsAndFillStreamsISO88591() throws Exception {
    final String getParams = "qt=%FC&dup=foo&ie=iso-8859-1&dup=%FC",
        postParams = "qt2=%FC&q=hello&d%75p=bar";
    final byte[] postBytes = postParams.getBytes(StandardCharsets.US_ASCII);
    final String contentType = "application/x-www-form-urlencoded; charset=iso-8859-1";

    // Set up the expected behavior
    HttpServletRequest request = getMock("/solr/select", contentType, postBytes.length);
    when(request.getMethod()).thenReturn("POST");
    when(request.getQueryString()).thenReturn(getParams);
    when(request.getInputStream()).thenReturn(new ByteServletInputStream(postBytes));

    MultipartRequestParser multipart = new MultipartRequestParser(2048);
    RawRequestParser raw = new RawRequestParser();
    FormDataRequestParser formdata = new FormDataRequestParser(2048);
    StandardRequestParser standard = new StandardRequestParser(multipart, raw, formdata);

    SolrParams p = standard.parseParamsAndFillStreams(request, new ArrayList<ContentStream>());

    assertEquals("contentType: " + contentType, "hello", p.get("q"));
    assertEquals("contentType: " + contentType, "\u00FC", p.get("qt"));
    assertEquals("contentType: " + contentType, "\u00FC", p.get("qt2"));
    assertArrayEquals(
        "contentType: " + contentType, new String[] {"foo", "\u00FC", "bar"}, p.getParams("dup"));

    verify(request).getInputStream();
  }

  @Test
  public void testStandardFormdataUploadLimit() throws Exception {
    final int limitKBytes = 128;

    final StringBuilder large = new StringBuilder("q=hello");
    // grow exponentially to reach 128 KB limit:
    while (large.length() <= limitKBytes * 1024) {
      large.append('&').append(large);
    }
    HttpServletRequest request = getMock("/solr/select", "application/x-www-form-urlencoded", -1);
    when(request.getMethod()).thenReturn("POST");
    when(request.getInputStream())
        .thenReturn(
            new ByteServletInputStream(large.toString().getBytes(StandardCharsets.US_ASCII)));

    FormDataRequestParser formdata = new FormDataRequestParser(limitKBytes);
    SolrException e =
        expectThrows(
            SolrException.class,
            () -> {
              formdata.parseParamsAndFillStreams(request, new ArrayList<>());
            });
    assertTrue(e.getMessage().contains("upload limit"));
    assertEquals(400, e.code());
    verify(request).getInputStream();
  }

  @Test
  public void testParameterIncompatibilityException1() throws Exception {
    HttpServletRequest request = getMock("/solr/select", "application/x-www-form-urlencoded", 100);
    // we emulate Jetty that returns empty stream when parameters were parsed before:
    when(request.getInputStream())
        .thenReturn(
            new ServletInputStream() {
              @Override
              public int read() {
                return -1;
              }

              @Override
              public boolean isFinished() {
                return true;
              }

              @Override
              public boolean isReady() {
                return true;
              }

              @Override
              public void setReadListener(ReadListener readListener) {}
            });

    FormDataRequestParser formdata = new FormDataRequestParser(2048);
    SolrException e =
        expectThrows(
            SolrException.class,
            () -> {
              formdata.parseParamsAndFillStreams(request, new ArrayList<>());
            });
    assertTrue(e.getMessage().startsWith("Solr requires that request parameters"));
    assertEquals(500, e.code());
    verify(request).getInputStream();
  }

  @Test
  public void testParameterIncompatibilityException2() throws Exception {
    HttpServletRequest request = getMock("/solr/select", "application/x-www-form-urlencoded", 100);
    when(request.getMethod()).thenReturn("POST");
    // we emulate Tomcat that throws IllegalStateException when parameters were parsed before:
    when(request.getInputStream()).thenThrow(new IllegalStateException());

    FormDataRequestParser formdata = new FormDataRequestParser(2048);
    SolrException e =
        expectThrows(
            SolrException.class,
            () -> {
              formdata.parseParamsAndFillStreams(request, new ArrayList<>());
            });
    assertTrue(e.getMessage().startsWith("Solr requires that request parameters"));
    assertEquals(500, e.code());
    verify(request).getInputStream();
  }

  public void testPostMissingContentType() throws Exception {
    HttpServletRequest request = getMock();
    when(request.getMethod()).thenReturn("POST");

    SolrRequestParsers parsers = new SolrRequestParsers(h.getCore().getSolrConfig());
    try {
      parsers.parse(h.getCore(), "/select", request);
    } catch (SolrException e) {
      log.error("should not throw SolrException", e);
      fail("should not throw SolrException");
    }
  }

  @Test
  public void testAutoDetect() throws Exception {
    String curl = "curl/7.30.0";
    for (String method : new String[] {"GET", "POST"}) {
      doAutoDetect(null, method, "{}=a", null, "{}", "a"); // unknown agent should not auto-detect
      doAutoDetect(curl, method, "{}", "application/json", null, null); // curl should auto-detect
      doAutoDetect(
          curl,
          method,
          "  \t\n\r  {}  ",
          "application/json",
          null,
          null); // starting with whitespace
      doAutoDetect(
          curl,
          method,
          "  \t\n\r  // how now brown cow\n {}  ",
          "application/json",
          null,
          null); // supporting comments
      doAutoDetect(
          curl,
          method,
          "  \t\n\r  #different style comment\n {}  ",
          "application/json",
          null,
          null);
      doAutoDetect(
          curl, method, "  \t\n\r  /* C style comment */\n {}  ", "application/json", null, null);
      doAutoDetect(curl, method, "  \t\n\r  <tag>hi</tag>  ", "text/xml", null, null);

      doAutoDetect(
          curl,
          method,
          "  \t\r\n  aaa=1&bbb=2&ccc=3",
          null,
          "bbb",
          "2"); // params with whitespace first
      doAutoDetect(
          curl,
          method,
          "/x=foo&aaa=1&bbb=2&ccc=3",
          null,
          "/x",
          "foo"); // param name that looks like a path
      doAutoDetect(
          curl,
          method,
          " \t\r\n /x=foo&aaa=1&bbb=2&ccc=3",
          null,
          "bbb",
          "2"); // param name that looks like a path
    }
  }

  public void doAutoDetect(
      String userAgent,
      String method,
      final String body,
      String expectedContentType,
      String expectedKey,
      String expectedValue)
      throws Exception {
    String uri = "/solr/select";
    String contentType = "application/x-www-form-urlencoded";
    int contentLength = -1; // does this mean auto-detect?

    HttpServletRequest request = mock(HttpServletRequest.class);
    when(request.getHeader("User-Agent")).thenReturn(userAgent);
    when(request.getRequestURI()).thenReturn(uri);
    when(request.getContentType()).thenReturn(contentType);
    when(request.getContentLength()).thenReturn(contentLength);

    when(request.getMethod()).thenReturn(method);
    // we don't pass a content-length to let the security mechanism limit it:
    when(request.getQueryString()).thenReturn("foo=1&bar=2");
    when(request.getInputStream())
        .thenReturn(new ByteServletInputStream(body.getBytes(StandardCharsets.US_ASCII)));

    SolrRequestParsers parsers = new SolrRequestParsers(h.getCore().getSolrConfig());
    SolrQueryRequest req = parsers.parse(h.getCore(), "/select", request);
    int num = 0;
    if (expectedContentType != null) {
      for (ContentStream cs : req.getContentStreams()) {
        num++;
        assertTrue(cs.getContentType().startsWith(expectedContentType));
        assertEquals(body, StrUtils.stringFromReader(cs.getReader()));
      }
      assertEquals(1, num);
    }

    assertEquals("1", req.getParams().get("foo"));
    assertEquals("2", req.getParams().get("bar"));

    if (expectedKey != null) {
      assertEquals(expectedValue, req.getParams().get(expectedKey));
    }

    req.close();
    verify(request).getInputStream();
  }

  public HttpServletRequest getMock() {
    return getMock("/solr/select", null, -1);
  }

  public HttpServletRequest getMock(String uri, String contentType, int contentLength) {
    HttpServletRequest request = mock(HttpServletRequest.class);
    when(request.getRequestURI()).thenReturn(uri);
    when(request.getContentType()).thenReturn(contentType);
    when(request.getContentLength()).thenReturn(contentLength);
    return request;
  }
}
